import { Client } from 'ssh2';
import { all } from 'inversify-express-utils';
import { makeLabelLogger } from './logger';
import { Socket } from 'net';
import { sleep, arrayRemove } from './tools';

let { llog } = makeLabelLogger('sshclient');
export class SSHClientEx extends Client {
	public static all = new Map<string, SSHClientEx>();
	public static create(serverName: string, remoteUser: string, remoteIP: string, sshKey: string) {
		let sc = new SSHClientEx(serverName, remoteUser, remoteIP, sshKey);
		SSHClientEx.all.set(serverName, sc);
		return sc;
	}
	public static get(serverName: string) {
		let sc = SSHClientEx.all.get(serverName);
		return sc;
	}
	public static delete(serverName: string) {
		let sc = SSHClientEx.all.get(serverName);
		if (sc) {
			sc.end();
			SSHClientEx.all.delete(serverName);
		}
	}

	/////////////////////////
	public serverName: string;
	public sshIP: string;
	public sshUser: string;
	public sshKey: string;
	public connected: boolean;
	private connecting: boolean;

	remoteForwards: Array<{ remotePort: number; destIP: string; destPort: number }> = [];
	public remoteChannels = [];

	constructor(name: string, user: string, ip: string, key: string) {
		super();
		this.serverName = name;
		this.sshUser = user;
		this.sshIP = ip;
		this.sshKey = key;
		this.connected = false;
		this.connecting = false;

		let uip = ip.split('@');
		if (uip.length == 2) {
			this.sshUser = uip[0];
			this.sshIP = uip[1];
		}

		this.reconnect();
	}

	_perform_reconnecting: boolean;
	public isConnecting = () => {
		if (this._perform_reconnecting) return true;
		return this.connecting;
	};

	setup() {
		this.on('error', function(e) {
			llog('sshclient error', e);
			this.connecting = false;
		})
			.on('end', () => {
				llog('sshclient end');
				this.connected = false;
				this.connecting = false;
				this.remoteChannels = [];
			})
			.on('ready', async () => {
				llog('ssh connect ready');
				this.connected = true;
				this.connecting = false;
				this.refreshRemoteForwards();
			})
			.on('tcp connection', (info, accept) => {
				llog('forward in', info.srcIP, info.srcPort, info.destIP, info.destPort);

				let cfg = this.remoteForwards.find(c => c.remotePort == info.destPort);
				if (cfg) {
					let srcSocket = new Socket();
					srcSocket.connect(cfg.destPort, cfg.destIP, () => {
						llog('forward connect ok');
						let remoteChannel = accept();
						srcSocket.pipe(remoteChannel).pipe(srcSocket);
						this.remoteChannels.push([remoteChannel, info.destPort]);
						remoteChannel.on('close', () => {
							arrayRemove(this.remoteChannels, e => e[0] == remoteChannel);
						});
					});
					srcSocket.on('error', () => {
						llog('forward connect fail');
					});
				}
			});
	}

	reconnect = async () => {
		this._perform_reconnecting = true;

		if (this.connected) {
			this.end();
		}
		this.removeAllListeners();
		this.setup();
		await sleep(1000);
		this.connect({
			port: 22,
			host: this.sshIP,
			username: this.sshUser,
			privateKey: this.sshKey,
		});

		this.once('ready', () => {
			this._perform_reconnecting = false;
		})
			.once('error', () => {
				this._perform_reconnecting = false;
			})
			.once('end', () => {
				this._perform_reconnecting = false;
			});
	};

	refreshRemoteForwards() {
		this.remoteForwards.forEach(async ({ remotePort, destIP, destPort }) => {
			try {
				// s检测目标端口是否已占用
				let [code, stdout] = await this.execute(`sudo /usr/sbin/lsof -i :${remotePort} | sed -n '2p'| tr -s ' '|cut -d' ' -f 2`);
				if (stdout) {
					llog('found port occupied, kill that process!', stdout);
					await this.execute(`sudo kill ${stdout}`);
				}

				this.forwardIn('0.0.0.0', remotePort, err => {
					if (err) {
						llog('foward error', err);
						this.end();
						return;
					}
					llog(`Listening for connections on server on port ${remotePort}->${destPort}!`);
				});
			} catch (e) {
				llog('makeforward err', e);
			}
		});
	}

	public addRemoteForward(remotePort: number, destIP: string, destPort: number) {
		this.remoteForwards.push({ remotePort, destIP, destPort });
		if (this.connected) this.refreshRemoteForwards();
	}

	execute = async function(cmd: string): Promise<Array<any>[3]> {
		return await new Promise(fill => {
			let stdout, stderr;
			llog('===>sshexecute begin:', cmd);
			this.exec(cmd, function(err, stream) {
				if (err) throw err;
				stream
					.on('close', function(code, signal) {
						llog('<===sshexecute end', code, signal);
						fill([code, stdout, stderr]);
					})
					.on('data', function(data: Buffer) {
						stdout = data.toString();
						llog('======stdout>', stdout);
					})
					.stderr.on('data', function(data: Buffer) {
						stderr = data.toString();
						llog('======stderr>', stderr);
					});
			});
		});
	};

	sshUpload = async function(src: string, dst: string): Promise<string> {
		return await new Promise(fill => {
			this.sftp(function(err, sftp) {
				if (err) {
					fill(err.message);
					return;
				}
				sftp.fastPut(src, dst, err => {
					if (err) {
						fill(err);
						return;
					}
					fill();
				});
			});
		});
	};

	sshSetUserServerKeyValue = async function(key: string, value: string) {
		return await this.execute(`docker run --rm --network=container:mb-user-redis redis redis-cli -h redis set ${key} ${value}`);
	};
}
